package com.nipun.neural.engine.runners;

import java.util.ArrayList;
import java.util.List;

import com.nipun.neural.engine.NetworkRunner;
import com.nipun.neural.objects.Network;
import com.nipun.neural.objects.Neuron;
import com.nipun.neural.objects.Synapse;


public class SimpleNetworkRunner implements NetworkRunner {
	
	private Network net;
	
	private Object[] edges;
	
	private Object[] vertices;
	
	private Object[] firingOrder;
	
	private Object[] inputNodes;
	
	private Object[] outputNodes;
	
	private Object[] memorynodes;
	
	private double correctionCoeff = 1;
	
	public SimpleNetworkRunner(Network network){
		net = network;
		edges = network.getEdges();
		vertices = network.getVertices();
		firingOrder = network.getFiringOrder();
		inputNodes = network.getInputNodes();
		outputNodes = network.getOutputNodes();
		memorynodes = network.getMemoryNodes();
	}
	
	/**
	 * expects a list of Double
	 * @param inputs
	 * @return
	 */
	public List run(List inputs){
		if(inputs.size() != inputNodes.length){
			System.out.println("size of input is not the same as required inputs");
			System.out.println("input nodes = "+inputNodes.length);
			System.out.println("given inputs = "+inputs.size());
			return null;
		}
		List result = new ArrayList();
		for(int i=0; i<inputNodes.length;i++){
			Neuron curr = (Neuron)inputNodes[i];
			curr.setActivation(((Double)inputs.get(i)).doubleValue());
		}
		for(int i=0; i<firingOrder.length;i++){
			Neuron n = (Neuron) firingOrder[i];
			n.fire(getEffectiveInput(n));
		}
		for(int i=0; i<outputNodes.length;i++){
			double temp = ((Neuron)outputNodes[i]).getActivation();
			result.add(new Double(temp));
		}
		if(memorynodes!=null){
			for(int i=0; i<memorynodes.length; i++){
				Neuron n = (Neuron) memorynodes[i];
				n.setActivation(getEffectiveInput(n));
			}
		}
		return result;
	}
	
	public void backPropagate(List expectations){
		/*
		 * errors is a list of Double
		 */
		if (expectations.size() != outputNodes.length){
			System.out.println("list of errors is not the same size as list of output neurons...exiting");
			return;
		}
		Object[] expts = expectations.toArray();
		Object[] revFiringOrder = reverseArray(firingOrder);
		for(int i=0; i<revFiringOrder.length;i++){
			Neuron n = (Neuron)revFiringOrder[i];
			if(isOutput(n)){
				double expVal = ((Double)expts[i]).doubleValue();
				double delta = (expVal - n.getActivation())*n.getDerivative();
				n.setDelta(delta);
				List neuronsBefore = neuronsBefore(n);
				for(int j=0; j<neuronsBefore.size();j++){
					Neuron tempNeuron = (Neuron)neuronsBefore.get(j);
					Synapse syn = getSynapseBetween(tempNeuron, n);
					double newWeight = syn.getWeight() + correctionCoeff*n.getDelta()*tempNeuron.getActivation();
					syn.setWeight(newWeight);
				}
				n.setBias(n.getBias() + correctionCoeff*n.getDelta());
			}else{
				/*
				 * ok...hidden unit. first we calculate the delta
				 */
				double delta = 0;
				List neuronsAfter = neuronsAfter(n);
				for(int j=0; j<neuronsAfter.size();j++){
					Neuron tempNeuron = (Neuron)neuronsAfter.get(j);
					Synapse syn = getSynapseBetween(n, tempNeuron);
					if(syn == null){
						System.out.println("a synapse was found null and should not have been");
						return;
					}
					delta += tempNeuron.getDelta()*syn.getWeight();
				}
				delta *= n.getDerivative();
				n.setDelta(delta);
				List neuronsBefore = neuronsBefore(n);
				for(int j=0; j<neuronsBefore.size();j++){
					Neuron tempNeuron = (Neuron)neuronsBefore.get(j);
					Synapse syn = getSynapseBetween(tempNeuron, n);
					double newWeight = syn.getWeight() + correctionCoeff*n.getDelta()*tempNeuron.getActivation();
					syn.setWeight(newWeight);
				}
				n.setBias(n.getBias() + correctionCoeff*n.getDelta());
			}
		}
	}
	
	private List neuronsAfter(Neuron n){
		List result = new ArrayList();
		for(int i=0; i<edges.length;i++){
			Synapse e = (Synapse)edges[i];
			if(e.getNode1().getLabel().equals(n.getLabel())){
				result.add(e.getNode2());
			}
		}
		return result;
	}
	
	private List neuronsBefore(Neuron n){
		List result = new ArrayList();
		for(int i=0; i<edges.length;i++){
			Synapse e = (Synapse)edges[i];
			if(e.getNode2().getLabel().equals(n.getLabel())){
				result.add(e.getNode1());
			}
		}
		return result;
	}
	
	private double getEffectiveInput(Neuron n){
		double result = 0;
		for(int i=0; i<edges.length;i++){
			Synapse e = (Synapse)edges[i];
			if(e.getNode2().getLabel().equals(n.getLabel())){
				result += e.getNode1().getActivation()*e.getWeight();
			}
		}
		return result;
	}
	
	private Synapse getSynapseBetween(Neuron n1, Neuron n2){
		Synapse syn = null;
		for(int i=0; i<edges.length;i++){
			Synapse e = (Synapse)edges[i];
			if(e.getNode1().equals(n1) && e.getNode2().equals(n2)){
				syn = e;
				break;
			}
		}
		return syn;
	}
	
	private boolean isOutput(Neuron n){
		for(int i=0; i<outputNodes.length;i++){
			Neuron temp = (Neuron)outputNodes[i];
			if(temp.equals(n)){
				return true;
			}
		}
		return false;
	}
	
	private Object[] reverseArray(Object[] arr){
		Object[] rev = new Object[arr.length];
		for(int i=0; i<arr.length; i++){
			rev[arr.length -1 -i] = arr[i];
		}
		return rev;
	}
	
}
